/*
 * Copyright 2013 Krzysztof Smigielski
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.nilcaream.dirstore.core.processor;

import com.google.common.base.Predicate;
import com.google.common.collect.*;
import com.nilcaream.dirstore.core.common.ProgressMeter;
import com.nilcaream.dirstore.core.common.ZipTool;
import com.nilcaream.dirstore.core.model.ContentId;
import com.nilcaream.dirstore.core.model.ZipDir;
import org.slf4j.Logger;

import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.zip.ZipFile;

/**
 * TODO
 * <p/>
 * Krzysztof Smigielski 7/9/13 2:51 PM
 */
public class DuplicateDirsFinder {

    @Inject
    private ZipTool zipTool;

    @Inject
    private ProgressMeter progressMeter;

    @Inject
    private Logger logger;

    Multimap<ContentId, ZipDir> createDuplicatedContentIdToZipDirsMap(List<File> files) throws IOException {
        Map<ZipDir, ContentId> zipDirToContentId = createZipDirToContentIdMap(files);

        Multimap<ContentId, ZipDir> idToZipDirs = invert(zipDirToContentId);
        Multimap<ContentId, ZipDir> duplicatesOnlyIdToZipDirs = filterNonDuplicates(idToZipDirs);
        Multimap<ContentId, ZipDir> duplicatedRootAwareIdToZipDirs = filterDuplicatedRoots(duplicatesOnlyIdToZipDirs);

        // filtered map copy, without references to the original map
        return HashMultimap.create(duplicatedRootAwareIdToZipDirs);
    }

    public Map<ZipDir, ContentId> createZipDirToContentIdMap(List<File> files) throws IOException {
        Map<ZipDir, ContentId> zipDirToContentId = Maps.newHashMapWithExpectedSize(files.size() * 100);

        for (File file : files) {
            logger.info("reading file " + file.getAbsolutePath());
            try (ZipFile zipFile = new ZipFile(file)) {
                Map<String, ContentId> directoryToContentId = zipTool.createDirectoryToContentIdMap(zipFile);
                for (Map.Entry<String, ContentId> dirToId : directoryToContentId.entrySet()) {
                    ZipDir zipDir = new ZipDir(file.getName(), dirToId.getKey());
                    ContentId contentId = dirToId.getValue();
                    zipDirToContentId.put(zipDir, contentId);
                }
            }
        }

        return zipDirToContentId;
    }

    public Multimap<ContentId, ZipDir> invert(Map<ZipDir, ContentId> zipDirToContentId) {
        return Multimaps.invertFrom(Multimaps.forMap(zipDirToContentId), HashMultimap.<ContentId, ZipDir>create());
    }

    public Multimap<ContentId, ZipDir> filterNonDuplicates(final Multimap<ContentId, ZipDir> idToZipDirs) {
        Multimap<ContentId, ZipDir> result = Multimaps.filterKeys(idToZipDirs, new Predicate<ContentId>() {
            @Override
            public boolean apply(ContentId contentId) {
                return idToZipDirs.get(contentId).size() > 1;
            }
        });
        return HashMultimap.create(result);
    }

    public Multimap<ContentId, ZipDir> filterDuplicatedRoots(final Multimap<ContentId, ZipDir> duplicatesOnlyIdToZipDirs) {
        logger.debug("reducing total number of zip directories from " + duplicatesOnlyIdToZipDirs.size());

        Multimap<ContentId, ZipDir> result = Multimaps.filterKeys(duplicatesOnlyIdToZipDirs, new Predicate<ContentId>() {
            @Override
            public boolean apply(ContentId currentId) {
                List<ZipDir> currentDirs = ImmutableList.copyOf(duplicatesOnlyIdToZipDirs.get(currentId));
                for (Map.Entry<ContentId, Collection<ZipDir>> anyEntry : duplicatesOnlyIdToZipDirs.asMap().entrySet()) {
                    Collection<ZipDir> anyDirs = anyEntry.getValue();
                    if (!currentId.equals(anyEntry.getKey())) {
                        List<ZipDir> anyDirsList = ImmutableList.copyOf(anyDirs);
                        if (isRootOf(anyDirsList, currentDirs)) {
                            return false;
                        }
                    }
                }
                return true;
            }
        });

        HashMultimap<ContentId, ZipDir> map = HashMultimap.create(result);
        logger.debug("reduced total number of zip directories to " + result.size());
        return map;
    }

    public Multimap<ContentId, ZipDir> filterBySize(final Multimap<ContentId, ZipDir> map, final long minimumSize) {
        Multimap<ContentId, ZipDir> result = Multimaps.filterKeys(map, new Predicate<ContentId>() {
            @Override
            public boolean apply(ContentId contentId) {
                return contentId.getSize() >= minimumSize;
            }
        });
        return HashMultimap.create(result);
    }

    public List<ContentId> orderBySizeDesc(final Set<ContentId> unorderedIds) {
        List<ContentId> ids = Lists.newArrayList(unorderedIds);
        Collections.sort(ids, new Comparator<ContentId>() {
            @Override
            public int compare(ContentId o1, ContentId o2) {
                return (int) (o2.getSize() - o1.getSize());
            }
        });
        return ids;
    }

    public long calculateTotalSizeOfDuplicates(final Multimap<ContentId, ZipDir> map) {
        long size = 0;
        for (Map.Entry<ContentId, Collection<ZipDir>> entry : map.asMap().entrySet()) {
            size += entry.getKey().getSize() * (entry.getValue().size() - 1);
        }
        return size;
    }

    private boolean isRootOf(List<ZipDir> parents, List<ZipDir> children) {
        for (ZipDir child : children) {
            boolean parentFound = false;
            for (ZipDir parent : parents) {
                if (parent.isRootOf(child)) {
                    parentFound = true;
                    break;
                }
            }
            if (!parentFound) {
                return false;
            }
        }
        return true;
    }
}
